package cn.s1995.sloth.bizs.aspect;


import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import cn.s1995.sloth.bizs.aspect.config.ApiLogAspectConfig;
import cn.s1995.sloth.bizs.entity.ApiLog;
import cn.s1995.sloth.bizs.service.ApiLogService;
import cn.s1995.sloth.common.core.util.StringUtil;
import cn.s1995.sloth.common.core.util.WebUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jodd.net.HttpMethod;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * ApiLogAspect
 *
 * @author penf
 * @description 接口日志定制化切面类
 * @date 2020/04/16 14:34
 */
@Slf4j
@Aspect
@Order(-10)
@Component
public class ApiLogAspect {

    /**
     * 日期时间格式yyyy/MM/dd HH:mm:ss SSS
     */
    private static String DATE_MSEL_TIME_PATTERN = "yyyy/MM/dd HH:mm:ss SSS";

    @Value("${server.servlet.context-path}")
    private String contextPath;

    @Resource
    private ApiLogService apiLogService;

    @Resource
    private ApiLogAspectConfig config;


    /**
     * 切点-定义一个切入点，可以是一个规则表达式，比如下例中某个 package 下的所有函数，也可以是一个注解等
     * <p>
     * execution() 为表达式主体
     * 第一个 * 号的位置：表示返回值类型，* 表示所有类型
     * 包名：表示需要拦截的包名，后面的两个句点表示当前包和当前包的所有子包，com.itcodai.course09.controller 包、子包下所有类的方法
     * 第二个 * 号的位置：表示类名，* 表示所有类
     * *(..) ：这个星号表示方法名，* 表示所有的方法，后面括弧里面表示方法的参数，两个句点表示任何参数
     */
    @Pointcut("execution(* cn.s1995.sloth.*.controller..*.*(..))")
    public void aspect() {
    }

    /**
     * 环绕-在切入点前后切入内容，并自己控制何时执行切入点自身的内容(不与@AfterThrowing混用)
     *
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("aspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // log.info("aspect around start...");
        // 请求开始时间
        LocalDateTime start = LocalDateTime.now();
        // 获取当前的HttpServletRequest对象
        // 注意：请求中的 body 内容仅能调用 request.getInputStream()， request.getReader()和request.getParameter("key") 方法读取一次
        // 重复读取会报 java.io.IOException: Stream closed 异常。
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        // proceed
        Object rest = point.proceed();

        // 记录接口日志
        recordApiLog(rest, start, request, point);

        // log.info("aspect around end...");
        return rest;
    }

    /**
     * 记录接口日志
     *
     * @param rest
     * @param start
     * @param request
     * @param point
     */
    private void recordApiLog(Object rest, LocalDateTime start, HttpServletRequest request, ProceedingJoinPoint point) {
        if (WebUtils.matchPath(request.getRequestURI().replaceFirst(contextPath, ""), config.getIgnoreUrls())) {
            return;
        }
        String method = request.getMethod();

        LocalDateTime end = LocalDateTime.now();
        ApiLog entity = new ApiLog();
        entity.setMethod(method);
        entity.setUri(request.getRequestURI());
        entity.setIp(request.getRemoteAddr());
        entity.setHeaders(WebUtils.getRequestHeaders(request));
        entity.setQuery(request.getQueryString() != null ? request.getQueryString() : "");
        entity.setBody(getParameter(point));
        entity.setResponse(JSONUtil.toJsonStr(rest));
        entity.setBeginDate(DateUtil.format(start, DATE_MSEL_TIME_PATTERN));
        entity.setEndDate(DateUtil.format(end, DATE_MSEL_TIME_PATTERN));
        entity.setElapsedTime(Duration.between(start, end).toMillis() + "ms");

        // GET请求不记录日志
        if (HttpMethod.GET.equalsName(method)) {
            // do nothing
        } else {
            // 输出日志
            log.info(entity.format());
        }

        // 保存入库
        if (WebUtils.matchPath(request.getRequestURI().replaceFirst(contextPath, ""), config.getSaveDbUrls())) {
            apiLogService.saveLog(entity);
        }
    }

    /**
     * 根据方法和传入的参数获取请求参数
     * @param point
     * @return
     */
    public static String getParameter(ProceedingJoinPoint point) {
        List<Object> argsList = new ArrayList<>();

        Method method = ((MethodSignature) point.getSignature()).getMethod();
        Parameter[] parameters = method.getParameters();

        for (int i = 0; i < parameters.length; i++) {
            // 将RequestBody注解修饰的参数作为请求参数
            RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
            if (requestBody != null) {
                argsList.add(point.getArgs()[i]);
            }
            // 将RequestParam注解修饰的参数作为请求参数
            RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
            if (requestParam != null) {
                Map<String, Object> map = new HashMap<>();
                String key = parameters[i].getName();
                if (StringUtil.isNotEmpty(requestParam.value())) {
                    key = requestParam.value();
                }
                if (point.getArgs()[i] instanceof MultipartFile
                        || point.getArgs()[i] instanceof MultipartFile[]) {
                    map.put(key, point.getArgs()[i].toString());
                } else {
                    map.put(key, point.getArgs()[i]);
                }

                argsList.add(map);
            }
        }

        if (argsList.size() == 0) {
            return "";
        } else if (argsList.size() == 1) {
            return JSONUtil.toJsonStr(argsList.get(0));
        } else {
            return JSONUtil.toJsonStr(argsList);
        }
    }

}
